package com.uxsino.commons.utils.config;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Stack;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * @see http
 *      ://stackoverflow.com/questions/4915422/get-line-number-from-xml-node-
 *      java
 */
public class PositionalXMLReader {
    public final static String LINE_NUMBER_KEY_NAME = "ln";

    public final static String SOURCE_NAME = "src";

    static Logger logger = LoggerFactory.getLogger(PositionalXMLReader.class);

    public static Document readXMLFromString(String docString, String sourceName) {
        InputStream in = new ByteArrayInputStream(docString.getBytes(StandardCharsets.UTF_8));
        return readXML(in, sourceName);
    }

    public static Document readXMLFromFile(File file, String sourceName) throws FileNotFoundException {

        FileInputStream in = null;
        try {
            in = new FileInputStream(file);
            return readXML(in, sourceName);
        } finally {
            if (null != in)
                try {
                    in.close();
                } catch (IOException e) {
                }
        }
    }

    public static Document readXML(InputStream in, String sourceName) {
        final Document doc;

        try {
            SAXParser parser = null;
            try {
                final SAXParserFactory factory = SAXParserFactory.newInstance();
                parser = factory.newSAXParser();
                final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
                final DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
                doc = docBuilder.newDocument();
                doc.setUserData(SOURCE_NAME, sourceName, null);
            } catch (final ParserConfigurationException e) {
                throw new RuntimeException("Can't create SAX parser / DOM builder.", e);
            }

            final Stack<Element> elementStack = new Stack<Element>();
            final StringBuilder textBuffer = new StringBuilder();

            final DefaultHandler handler = new DefaultHandler() {
                private Locator locator;

                @Override
                public void setDocumentLocator(final Locator locator) {
                    this.locator = locator; // Save the locator, so that it can
                                            // be
                                            // used later for line tracking when
                                            // traversing nodes.
                }

                @Override
                public void startElement(final String uri, final String localName, final String qName,
                    final Attributes attributes) throws SAXException {
                    addTextIfNeeded();
                    final Element el = doc.createElement(qName);
                    for (int i = 0; i < attributes.getLength(); i++) {
                        el.setAttribute(attributes.getQName(i), attributes.getValue(i));
                    }
                    el.setUserData(LINE_NUMBER_KEY_NAME, String.valueOf(this.locator.getLineNumber()), null);
                    elementStack.push(el);
                }

                @Override
                public void endElement(final String uri, final String localName, final String qName) {
                    addTextIfNeeded();
                    final Element closedEl = elementStack.pop();
                    if (elementStack.isEmpty()) { // Is this the root element?
                        doc.appendChild(closedEl);
                    } else {
                        final Element parentEl = elementStack.peek();
                        parentEl.appendChild(closedEl);
                    }
                }

                @Override
                public void characters(final char ch[], final int start, final int length) throws SAXException {
                    textBuffer.append(ch, start, length);
                }

                // Outputs text accumulated under the current node
                private void addTextIfNeeded() {
                    if (textBuffer.length() > 0) {
                        final Element el = elementStack.peek();
                        final Node textNode = doc.createTextNode(textBuffer.toString());
                        el.appendChild(textNode);
                        textBuffer.delete(0, textBuffer.length());
                    }
                }
            };

            parser.parse(in, handler);
            return doc;
        } catch (SAXParseException esax) {
            logger.error("reading exception: ", esax);
        } catch (Exception e) {
            logger.error("reading exception: ", e);
        }
        return null;
    }

    public static Document getNormalizedDocumentFromFile(File xmlFile, String sourceFile) {
        Document doc;
        try {
            doc = readXMLFromFile(xmlFile, sourceFile);
        } catch (Exception e) {
            logger.error("error loading " + sourceFile, e);
            return null;
        }
        if (doc == null || doc.getDocumentElement() == null) {
            return null;
        }

        doc.getDocumentElement().normalize();
        return doc;
    }

    public static Document getNormalizedDocumentFromStream(InputStream is, String sourceFile) {
        Document doc;
        try {
            doc = readXML(is, sourceFile);
        } catch (Exception e) {
            logger.error("error loading " + sourceFile, e);
            return null;
        }
        if (doc == null || doc.getDocumentElement() == null) {
            return null;
        }

        doc.getDocumentElement().normalize();
        return doc;
    }

    public static Document getNormalizedDocumentFromString(String docString, String sourceName) {
        Document doc;
        try {
            doc = readXMLFromString(docString, sourceName);
        } catch (Exception e) {
            logger.error("error loading " + sourceName, e);
            return null;
        }
        if (doc == null || doc.getDocumentElement() == null) {
            return null;
        }

        doc.getDocumentElement().normalize();
        return doc;
    }

}